{
 "cells": [
  {
   "cell_type": "markdown",
   "id": "592d977a-34b0-42f0-879c-1e8afe5cb134",
   "metadata": {},
   "source": [
    "# Normalize Datasets\n",
    "\n",
    "The Dataset Normalizer plugin is used to transform 'pandas-unfriendly' datasets (e.g., Excel files that do not follow a standard tabular structure) into a more suitable format for pandas. It is backed by an LLM that generates Python code to convert the original datasets into new ones.\n",
    "\n",
    "In `tablegpt-agent`, this plugin is used to better format 'pandas-unfriendly' datasets, making them more understandable for the subsequent steps. This plugin is optional; if used, it serves as the very first step in the [File Reading Workflow](../../explanation/file-reading), easing the difficulty of data analysis in the subsequent workflow.\n",
    "\n",
    "## Introduction\n",
    "\n",
    "The `Dataset Normalizer` is a specialized tool designed to tackle challenges that arise when working with irregular and poorly structured datasets. These challenges are especially prevalent in Excel files, which are often used as a flexible but inconsistent way of storing data.\n",
    "\n",
    "Analyzing Excel data files can pose significant challenges, such as:\n",
    "\n",
    "- **Irregular Formatting:** Datasets may lack a consistent tabular structure, with varying cell sizes or non-standard layouts.\n",
    "- **Merged Cells:** Cells spanning multiple rows or columns can disrupt parsing tools.\n",
    "- **Inconsistent Headers:** Columns may have incomplete, redundant, or nested headers.\n",
    "- **Hidden Data:** Data may be stored in additional sheets or rely on calculated fields that are not directly accessible.\n",
    "- **Mixed Data Types:** Columns may contain inconsistent data types, such as numbers mixed with text.\n",
    "- **Empty or Placeholder Rows:** Extra rows with missing or irrelevant data can complicate data loading and analysis."
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "> **!!! Note:** When the `tablegpt-agent` enables the `Dataset Normalizer` to format the dataset, the dataset reading process will be noticeably slower. This is because the `Dataset Normalizer` needs to analyze the dataset and generate transformation code, a process that takes considerable time. \n",
    ">\n",
    "> **It is worth noting that the data normalization process can effectively address most common data irregularities. However, for more complex datasets, further optimization may be needed, and the results depend on the specific normalization model used.**"
   ]
  },
  {
   "cell_type": "markdown",
   "id": "bc22a838",
   "metadata": {},
   "source": [
    "## Quick Start\n",
    "\n",
    "To enable the `Dataset Normalizer`, ensure you pass it as a parameter when creating the `tablegpt-agent`. You can follow the example below:"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": 7,
   "id": "7d892b2e-63ea-47bf-8bf7-ed4dcb7ed876",
   "metadata": {},
   "outputs": [],
   "source": [
    "from pathlib import Path\n",
    "from langchain_openai import ChatOpenAI\n",
    "from pybox import AsyncLocalPyBoxManager\n",
    "from tablegpt.agent import create_tablegpt_graph\n",
    "from tablegpt import DEFAULT_TABLEGPT_IPYKERNEL_PROFILE_DIR\n",
    "\n",
    "llm = ChatOpenAI(openai_api_base=\"YOUR_VLLM_URL\", openai_api_key=\"whatever\", model_name=\"TableGPT2-7B\")\n",
    "normalize_llm = ChatOpenAI(openai_api_base=\"YOUR_VLLM_URL\", openai_api_key=\"whatever\", model_name=\"YOUR_VLLM_MODEL_NAME\")\n",
    "pybox_manager = AsyncLocalPyBoxManager(profile_dir=DEFAULT_TABLEGPT_IPYKERNEL_PROFILE_DIR)\n",
    "\n",
    "agent = create_tablegpt_graph(\n",
    "    llm=llm,\n",
    "    pybox_manager=pybox_manager,\n",
    "    normalize_llm=normalize_llm,\n",
    "    session_id=\"some-session-id\", # This is required when using file-reading\n",
    ")"
   ]
  },
  {
   "cell_type": "markdown",
   "id": "d350ea84-a0d4-4780-a8e1-d483be53ecaa",
   "metadata": {},
   "source": [
    "Given an Excel file [产品生产统计表.xlsx](https://github.com/tablegpt/tablegpt-agent/blob/main/examples/datasets/产品生产统计表.xlsx) with merged cells and irregular headers:\n",
    "\n",
    "<table style=\"border: 1px solid black; border-collapse: collapse;\">\n",
    "  <thead>\n",
    "    <tr>\n",
    "      <th colspan=\"9\" style=\"text-align: center; font-size: 24px;\">产品生产统计表</th>\n",
    "    </tr>\n",
    "    <tr>\n",
    "      <th rowspan=\"2\">生产日期</th>\n",
    "      <th rowspan=\"2\">制造编号</th>\n",
    "      <th rowspan=\"2\">产品名称</th>\n",
    "      <th rowspan=\"2\">预定产量</th>\n",
    "      <th colspan=\"2\">本日产量</th>\n",
    "      <th rowspan=\"2\">累计产量</th>\n",
    "      <th colspan=\"2\">耗费工时</th>\n",
    "    </tr>\n",
    "    <tr>\n",
    "      <th>预计</th>\n",
    "      <th>实际</th>\n",
    "      <th>本日</th>\n",
    "      <th>累计</th>\n",
    "    </tr>\n",
    "  </thead>\n",
    "  <tbody>\n",
    "    <tr>\n",
    "      <td>2007/8/10</td>\n",
    "      <td>FK-001</td>\n",
    "      <td>猕猴桃果肉饮料</td>\n",
    "      <td>100000</td>\n",
    "      <td>40000</td>\n",
    "      <td>45000</td>\n",
    "      <td>83000</td>\n",
    "      <td>10</td>\n",
    "      <td>20</td>\n",
    "    </tr>\n",
    "    <tr>\n",
    "      <td>2007/8/11</td>\n",
    "      <td>FK-002</td>\n",
    "      <td>西瓜果肉饮料</td>\n",
    "      <td>100000</td>\n",
    "      <td>40000</td>\n",
    "      <td>44000</td>\n",
    "      <td>82000</td>\n",
    "      <td>9</td>\n",
    "      <td>18</td>\n",
    "    </tr>\n",
    "    <tr>\n",
    "      <td>2007/8/12</td>\n",
    "      <td>FK-003</td>\n",
    "      <td>草莓果肉饮料</td>\n",
    "      <td>100000</td>\n",
    "      <td>40000</td>\n",
    "      <td>45000</td>\n",
    "      <td>83000</td>\n",
    "      <td>9</td>\n",
    "      <td>18</td>\n",
    "    </tr>\n",
    "    <tr>\n",
    "      <td>2007/8/13</td>\n",
    "      <td>FK-004</td>\n",
    "      <td>蓝莓果肉饮料</td>\n",
    "      <td>100000</td>\n",
    "      <td>40000</td>\n",
    "      <td>45000</td>\n",
    "      <td>83000</td>\n",
    "      <td>9</td>\n",
    "      <td>18</td>\n",
    "    </tr>\n",
    "  </tbody>\n",
    "</table>"
   ]
  },
  {
   "cell_type": "markdown",
   "id": "2ccec619",
   "metadata": {},
   "source": [
    "Add the file for processing in the `additional_kwargs` of HumanMessage:"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": 8,
   "id": "d7a09def-56ca-4889-b240-1c9c31d8f7cb",
   "metadata": {},
   "outputs": [],
   "source": [
    "from typing import TypedDict\n",
    "from langchain_core.messages import HumanMessage\n",
    "\n",
    "class Attachment(TypedDict):\n",
    "    \"\"\"Contains at least one dictionary with the key filename.\"\"\"\n",
    "    filename: str\n",
    "\n",
    "attachment_msg = HumanMessage(\n",
    "    content=\"\",\n",
    "    # Please make sure your iPython kernel can access your filename.\n",
    "    additional_kwargs={\"attachments\": [Attachment(filename=\"产品生产统计表.xlsx\")]},\n",
    ")"
   ]
  },
  {
   "cell_type": "markdown",
   "id": "220d96fc",
   "metadata": {},
   "source": [
    "Invoke the `tablegpt-agent` to normalize the datasets:"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": 9,
   "id": "e7fe0dfa-633b-4cf4-b9fc-51a64afc24ff",
   "metadata": {},
   "outputs": [
    {
     "data": {
      "text/plain": [
       "[HumanMessage(content='', additional_kwargs={'attachments': [{'filename': '产品生产统计表.xlsx'}]}, response_metadata={}, id='452ef08b-a45f-49ce-8489-4062cd35d57c'),\n",
       " AIMessage(content='我已经收到您的数据文件，我需要查看文件内容以对数据集有一个初步的了解。首先我会读取数据到 `df` 变量中，并通过 `df.info` 查看 NaN 情况和数据类型。\\n```python\\n# Load the data into a DataFrame\\ndf = read_df(\\'产品生产统计表.xlsx\\')\\n# Normalize the data\\ntry:\\n    df = df.copy()\\n\\n    import pandas as pd\\n\\n    # Assuming the original data is loaded into a DataFrame named `df`\\n\\n    # Step 1: Isolate the Table Header\\n    # Remove the unnecessary top rows and columns\\n    df = df.drop([0, 1])  # Drop the first two rows which contain header information\\n    df = df.reset_index(drop=True)  # Reset the index to start from 0\\n\\n    # Step 2: Store the Result as `final_df`\\n    final_df = df\\n\\n    # Step 3: Rename Columns of final_df\\n    # Adjust the column names of final_df to match the desired format\\n    final_df.columns = [\\'生产日期\\', \\'制造编号\\', \\'产品名称\\', \\'预定产量\\', \\'本日产量预计\\', \\'本日产量实际\\', \\'累计产量\\', \\'本日耗费工时\\', \\'累计耗费工时\\']\\n\\n    # Step 4: Data Processing\\n    # Ensure the data types are correct and remove any unnecessary rows or columns\\n    final_df = final_df.dropna(subset=[\\'生产日期\\'])  # Remove rows where the production date is missing\\n    final_df = final_df.drop_duplicates()  # Remove duplicate rows if any\\n    final_df = final_df.reset_index(drop=True)  # Reset the index again to ensure it starts from 0\\n\\n    # Convert columns to appropriate data types\\n    final_df[\\'预定产量\\'] = final_df[\\'预定产量\\'].astype(int)\\n    final_df[\\'本日产量预计\\'] = final_df[\\'本日产量预计\\'].astype(int)\\n    final_df[\\'本日产量实际\\'] = final_df[\\'本日产量实际\\'].astype(int)\\n    final_df[\\'累计产量\\'] = final_df[\\'累计产量\\'].astype(int)\\n    final_df[\\'本日耗费工时\\'] = final_df[\\'本日耗费工时\\'].astype(int)\\n    final_df[\\'累计耗费工时\\'] = final_df[\\'累计耗费工时\\'].astype(int)\\n\\n    # Display the transformed DataFrame\\n    print(final_df)\\n    if final_df.columns.tolist() == final_df.iloc[0].tolist():\\n        final_df = final_df.iloc[1:]\\n\\n    # reassign df with the formatted DataFrame\\n    df = final_df\\nexcept Exception as e:\\n    # Unable to apply formatting to the original DataFrame. proceeding with the unformatted DataFrame.\\n    print(f\"Reformat failed with error {e}, use the original DataFrame.\")\\n# Remove leading and trailing whitespaces in column names\\ndf.columns = df.columns.str.strip()\\n\\n# Remove rows and columns that contain only empty values\\ndf = df.dropna(how=\\'all\\').dropna(axis=1, how=\\'all\\')\\n\\n# Get the basic information of the dataset\\ndf.info(memory_usage=False)\\n```', additional_kwargs={'parent_id': 'some-parent-id1', 'thought': '我已经收到您的数据文件，我需要查看文件内容以对数据集有一个初步的了解。首先我会读取数据到 `df` 变量中，并通过 `df.info` 查看 NaN 情况和数据类型。', 'action': {'tool': 'python', 'tool_input': '# Load the data into a DataFrame\\ndf = read_df(\\'产品生产统计表.xlsx\\')\\n# Normalize the data\\ntry:\\n    df = df.copy()\\n\\n    import pandas as pd\\n\\n    # Assuming the original data is loaded into a DataFrame named `df`\\n\\n    # Step 1: Isolate the Table Header\\n    # Remove the unnecessary top rows and columns\\n    df = df.drop([0, 1])  # Drop the first two rows which contain header information\\n    df = df.reset_index(drop=True)  # Reset the index to start from 0\\n\\n    # Step 2: Store the Result as `final_df`\\n    final_df = df\\n\\n    # Step 3: Rename Columns of final_df\\n    # Adjust the column names of final_df to match the desired format\\n    final_df.columns = [\\'生产日期\\', \\'制造编号\\', \\'产品名称\\', \\'预定产量\\', \\'本日产量预计\\', \\'本日产量实际\\', \\'累计产量\\', \\'本日耗费工时\\', \\'累计耗费工时\\']\\n\\n    # Step 4: Data Processing\\n    # Ensure the data types are correct and remove any unnecessary rows or columns\\n    final_df = final_df.dropna(subset=[\\'生产日期\\'])  # Remove rows where the production date is missing\\n    final_df = final_df.drop_duplicates()  # Remove duplicate rows if any\\n    final_df = final_df.reset_index(drop=True)  # Reset the index again to ensure it starts from 0\\n\\n    # Convert columns to appropriate data types\\n    final_df[\\'预定产量\\'] = final_df[\\'预定产量\\'].astype(int)\\n    final_df[\\'本日产量预计\\'] = final_df[\\'本日产量预计\\'].astype(int)\\n    final_df[\\'本日产量实际\\'] = final_df[\\'本日产量实际\\'].astype(int)\\n    final_df[\\'累计产量\\'] = final_df[\\'累计产量\\'].astype(int)\\n    final_df[\\'本日耗费工时\\'] = final_df[\\'本日耗费工时\\'].astype(int)\\n    final_df[\\'累计耗费工时\\'] = final_df[\\'累计耗费工时\\'].astype(int)\\n\\n    # Display the transformed DataFrame\\n    print(final_df)\\n    if final_df.columns.tolist() == final_df.iloc[0].tolist():\\n        final_df = final_df.iloc[1:]\\n\\n    # reassign df with the formatted DataFrame\\n    df = final_df\\nexcept Exception as e:\\n    # Unable to apply formatting to the original DataFrame. proceeding with the unformatted DataFrame.\\n    print(f\"Reformat failed with error {e}, use the original DataFrame.\")\\n# Remove leading and trailing whitespaces in column names\\ndf.columns = df.columns.str.strip()\\n\\n# Remove rows and columns that contain only empty values\\ndf = df.dropna(how=\\'all\\').dropna(axis=1, how=\\'all\\')\\n\\n# Get the basic information of the dataset\\ndf.info(memory_usage=False)'}, 'model_type': None}, response_metadata={}, id='8e5d0026-215d-46e2-ab60-9174c5bf50bd', tool_calls=[{'name': 'python', 'args': {'query': '# Load the data into a DataFrame\\ndf = read_df(\\'产品生产统计表.xlsx\\')\\n# Normalize the data\\ntry:\\n    df = df.copy()\\n\\n    import pandas as pd\\n\\n    # Assuming the original data is loaded into a DataFrame named `df`\\n\\n    # Step 1: Isolate the Table Header\\n    # Remove the unnecessary top rows and columns\\n    df = df.drop([0, 1])  # Drop the first two rows which contain header information\\n    df = df.reset_index(drop=True)  # Reset the index to start from 0\\n\\n    # Step 2: Store the Result as `final_df`\\n    final_df = df\\n\\n    # Step 3: Rename Columns of final_df\\n    # Adjust the column names of final_df to match the desired format\\n    final_df.columns = [\\'生产日期\\', \\'制造编号\\', \\'产品名称\\', \\'预定产量\\', \\'本日产量预计\\', \\'本日产量实际\\', \\'累计产量\\', \\'本日耗费工时\\', \\'累计耗费工时\\']\\n\\n    # Step 4: Data Processing\\n    # Ensure the data types are correct and remove any unnecessary rows or columns\\n    final_df = final_df.dropna(subset=[\\'生产日期\\'])  # Remove rows where the production date is missing\\n    final_df = final_df.drop_duplicates()  # Remove duplicate rows if any\\n    final_df = final_df.reset_index(drop=True)  # Reset the index again to ensure it starts from 0\\n\\n    # Convert columns to appropriate data types\\n    final_df[\\'预定产量\\'] = final_df[\\'预定产量\\'].astype(int)\\n    final_df[\\'本日产量预计\\'] = final_df[\\'本日产量预计\\'].astype(int)\\n    final_df[\\'本日产量实际\\'] = final_df[\\'本日产量实际\\'].astype(int)\\n    final_df[\\'累计产量\\'] = final_df[\\'累计产量\\'].astype(int)\\n    final_df[\\'本日耗费工时\\'] = final_df[\\'本日耗费工时\\'].astype(int)\\n    final_df[\\'累计耗费工时\\'] = final_df[\\'累计耗费工时\\'].astype(int)\\n\\n    # Display the transformed DataFrame\\n    print(final_df)\\n    if final_df.columns.tolist() == final_df.iloc[0].tolist():\\n        final_df = final_df.iloc[1:]\\n\\n    # reassign df with the formatted DataFrame\\n    df = final_df\\nexcept Exception as e:\\n    # Unable to apply formatting to the original DataFrame. proceeding with the unformatted DataFrame.\\n    print(f\"Reformat failed with error {e}, use the original DataFrame.\")\\n# Remove leading and trailing whitespaces in column names\\ndf.columns = df.columns.str.strip()\\n\\n# Remove rows and columns that contain only empty values\\ndf = df.dropna(how=\\'all\\').dropna(axis=1, how=\\'all\\')\\n\\n# Get the basic information of the dataset\\ndf.info(memory_usage=False)'}, 'id': 'af99b549-09f1-4623-930a-3ffbfde40216', 'type': 'tool_call'}]),\n",
       " ToolMessage(content=[{'type': 'text', 'text': \"```pycon\\nReformat failed with error cannot convert float NaN to integer, use the original DataFrame.\\n<class 'pandas.core.frame.DataFrame'>\\nRangeIndex: 26 entries, 0 to 25\\nData columns (total 9 columns):\\n #   Column  Non-Null Count  Dtype \\n---  ------  --------------  ----- \\n 0   生产日期    22 non-null     object\\n 1   制造编号    25 non-null     object\\n 2   产品名称    20 non-null     object\\n 3   预定产量    20 non-null     object\\n 4   本日产量预计  21 non-null     object\\n 5   本日产量实际  21 non-null     object\\n 6   累计产量    25 non-null     object\\n 7   本日耗费工时  20 non-null     object\\n 8   累计耗费工时  20 non-null     object\\ndtypes: object(9)\\n```\"}], name='python', id='58429576-461b-4a50-8c3d-5bbacca22cdb', tool_call_id='af99b549-09f1-4623-930a-3ffbfde40216', artifact=[]),\n",
       " AIMessage(content='接下来我将用 `df.head(5)` 来查看数据集的前 5 行。\\n```python\\n# Show the first 5 rows to understand the structure\\ndf.head(5)\\n```', additional_kwargs={'parent_id': 'some-parent-id1', 'thought': '接下来我将用 `df.head(5)` 来查看数据集的前 5 行。', 'action': {'tool': 'python', 'tool_input': '# Show the first 5 rows to understand the structure\\ndf.head(5)'}, 'model_type': None}, response_metadata={}, id='0c67c2b2-36d2-49cc-8fb8-7f6e5dfc8625', tool_calls=[{'name': 'python', 'args': {'query': '# Show the first 5 rows to understand the structure\\ndf.head(5)'}, 'id': 'a4ccd41a-a872-4ff0-aae5-678a96b9b54a', 'type': 'tool_call'}]),\n",
       " ToolMessage(content=[{'type': 'text', 'text': '```pycon\\n                  生产日期    制造编号     产品名称    预定产量 本日产量预计 本日产量实际   累计产量 本日耗费工时 累计耗费工时\\n0  2007-08-10 00:00:00  FK-001  猕猴桃果肉饮料  100000  40000  45000  83000     10     20\\n1  2007-08-11 00:00:00  FK-002   西瓜果肉饮料  100000  40000  44000  82000      9     18\\n2  2007-08-12 00:00:00  FK-003   草莓果肉饮料  100000  40000  45000  83000      9     18\\n3  2007-08-13 00:00:00  FK-004   蓝莓果肉饮料  100000  40000  45000  83000      9     18\\n4  2007-08-14 00:00:00  FK-005  水密桃果肉饮料  100000  40000  45000  83000     10     20\\n```'}], name='python', id='d828aa34-7c9e-4fee-8ae1-7b553530292b', tool_call_id='a4ccd41a-a872-4ff0-aae5-678a96b9b54a', artifact=[]),\n",
       " AIMessage(content='我已经了解了数据集 产品生产统计表.xlsx 的基本信息。请问我可以帮您做些什么？', additional_kwargs={'parent_id': 'some-parent-id1'}, response_metadata={}, id='e836eba6-9597-4bf8-acfd-2a81871916a6')]"
      ]
     },
     "execution_count": 9,
     "metadata": {},
     "output_type": "execute_result"
    }
   ],
   "source": [
    "from datetime import date\n",
    "from tablegpt.agent.file_reading import Stage\n",
    "\n",
    "# Reading and processing files.\n",
    "response = await agent.ainvoke(\n",
    "    input={\n",
    "        \"entry_message\": attachment_msg,\n",
    "        \"processing_stage\": Stage.UPLOADED,\n",
    "        \"messages\": [attachment_msg],\n",
    "        \"parent_id\": \"some-parent-id1\",\n",
    "        \"date\": date.today(),\n",
    "    },\n",
    "    config={\n",
    "        # Using checkpointer requires binding thread_id at runtime.\n",
    "        \"configurable\": {\"thread_id\": \"some-thread-id\"},\n",
    "    },\n",
    ")\n",
    "\n",
    "response[\"messages\"]"
   ]
  },
  {
   "cell_type": "markdown",
   "id": "478453cd",
   "metadata": {},
   "source": [
    "By formatting the content of the last `ToolMessage`, you can see the normalized data:\n",
    "\n",
    "<table style=\"border: 1px solid black; border-collapse: collapse;\">\n",
    "  <thead>\n",
    "    <tr>\n",
    "      <th>生产日期</th>\n",
    "      <th>制造编号</th>\n",
    "      <th>产品名称</th>\n",
    "      <th>预定产量</th>\n",
    "      <th>本日产量预计</th>\n",
    "      <th>本日产量实际</th>\n",
    "      <th>累计产量</th>\n",
    "      <th>本日耗费工时</th>\n",
    "      <th>累计耗费工时</th>\n",
    "    </tr>\n",
    "  </thead>\n",
    "  <tbody>\n",
    "    <tr>\n",
    "      <td>2007/8/10</td>\n",
    "      <td>FK-001</td>\n",
    "      <td>猕猴桃果肉饮料</td>\n",
    "      <td>100000</td>\n",
    "      <td>40000</td>\n",
    "      <td>45000</td>\n",
    "      <td>83000</td>\n",
    "      <td>10</td>\n",
    "      <td>20</td>\n",
    "    </tr>\n",
    "    <tr>\n",
    "      <td>2007/8/11</td>\n",
    "      <td>FK-002</td>\n",
    "      <td>西瓜果肉饮料</td>\n",
    "      <td>100000</td>\n",
    "      <td>40000</td>\n",
    "      <td>44000</td>\n",
    "      <td>82000</td>\n",
    "      <td>9</td>\n",
    "      <td>18</td>\n",
    "    </tr>\n",
    "    <tr>\n",
    "      <td>2007/8/12</td>\n",
    "      <td>FK-003</td>\n",
    "      <td>草莓果肉饮料</td>\n",
    "      <td>100000</td>\n",
    "      <td>40000</td>\n",
    "      <td>45000</td>\n",
    "      <td>83000</td>\n",
    "      <td>9</td>\n",
    "      <td>18</td>\n",
    "    </tr>\n",
    "    <tr>\n",
    "      <td>2007/8/13</td>\n",
    "      <td>FK-004</td>\n",
    "      <td>蓝莓果肉饮料</td>\n",
    "      <td>100000</td>\n",
    "      <td>40000</td>\n",
    "      <td>45000</td>\n",
    "      <td>83000</td>\n",
    "      <td>9</td>\n",
    "      <td>18</td>\n",
    "    </tr>\n",
    "  </tbody>\n",
    "</table>"
   ]
  }
 ],
 "metadata": {
  "kernelspec": {
   "display_name": ".venv",
   "language": "python",
   "name": "python3"
  },
  "language_info": {
   "codemirror_mode": {
    "name": "ipython",
    "version": 3
   },
   "file_extension": ".py",
   "mimetype": "text/x-python",
   "name": "python",
   "nbconvert_exporter": "python",
   "pygments_lexer": "ipython3",
   "version": "3.12.7"
  }
 },
 "nbformat": 4,
 "nbformat_minor": 5
}
